#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2011 BidiX (http://bidix.info)
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
"""
	Notewiki App uses :mod:`webapp2` framework. This module implements the webapp2 handlers. 
	
	The :mod:`webapp2` is a lightweight Python web framework compatible with Google App Engine’s webapp. 
	See `link webapp2 documentation <http://http://webapp-improved.appspot.com/>`_.
		
	The url router is implemented by :func:`notewiki.webapp.notewikiapp`. 
	
	The main webapp2 handlers are :
	
	- :class:`StaticPageHandler`: handle by this app all static resources
	- :class:`ArticleHandler` handle all article request
	- :class:`TagHandler` and :class:`TagListHandler` for tag request
	- :class:`XmlExport` and :class:`XmlImport` to export and import articles as XML documents.
	
"""
import logging
import mimetypes
import cgi
import re
import time, datetime

import webapp2
from google.appengine.api import users
from notewiki.model import Article, Role, area_for, extension, name_without_extension, user_role, Profile, tags_to_string
from notewiki.ui import template, text_to_html, render_user
from notewiki.ui import pages
import notewiki.ui
from operator import itemgetter

class StaticPageHandler(webapp2.RequestHandler):
	"""
	Handler for static resources.
	
	Resources are file located in 'static_root_path'
	"""
	expires = 60*60*24*15 # 15 days max-age (release period)

	def get(self, *groups):
		if self.request.path == '/favicon.ico':
			file_path = '/images/favicon.ico'
		else:
			file_path = self.request.path
		static_root_path = self.app.config.get('static_root_path')
		if file_path.endswith('.svg'):
			mime_type = 'image/svg+xml'
		elif file_path.endswith('.ico'):
			mime_type = 'image/x-icon'
		else:
			mime_type = mimetypes.guess_type(file_path)[0]

		try:
			f = open(static_root_path+file_path, 'r')
			self.response.headers['Content-Type'] = mime_type
			self.response.headers['Cache-Control'] = 'max-age=%s'%self.expires
			self.response.out.write(f.read())
		except IOError:
			logging.error("SaticPage not found : %s"%file_path)
			self.response.set_status(404)
			self.response.out.write(template.render("404", {"url": file_path}))
			
class ControledRequestHandler(webapp2.RequestHandler):
	"""
	Base class for handlers that control user access.
	"""
	def check_access(self, url, write_access = False):
		"""
		Check user access.
		"""
		self.user = users.get_current_user()
		return True

	def check_admin_access(self, url):
		"""
		Check user admin access.
		"""
		self.check_access(url)
		if users.is_current_user_admin():
			return True
		else:
			self.response.set_status(401)
			pages.ForbiddenPage(self.response, {'url': url})
			return False

			
class ArticleHandler(ControledRequestHandler):
	"""
	Handler for Articles and Folders
	"""
	file_expires = 60*60*24*1 # 1 day max-age for file

	def check_access(self, url, write_access = False):
		"""
		For all request, initialize variables used at access level
		"""
		self.user = users.get_current_user()
		self.url = url
		self.name = name_without_extension(self.url)
		self.name_extension = extension(self.url)
		self.command = self.request.query_string
		if self.name and (self.name[len(self.name)-1] == '/'):
			self.name = self.name[0:len(self.name)-1]
			self.command = 'list'
		self.area = area_for(self.name)
		self.user_role = user_role(self.name)
		if (self.user_role == Role.forbidden) or (write_access and (self.user_role == Role.reader)):
			self.response.set_status(401)
			pages.ForbiddenPage(self.response, {'url': self.name})
			return False
		else:
			self.context = {
				'app_name': self.app.config.get('wiki_app_name'),
				'path': self.request.path_info,
				'command': self.command,
				'user': self.app.config.get('format_user_fn')(self.request.url, self.app.config.get('wiki_base_url')),
				'username': render_user(self.user),
				'role': self.user_role,
				'msg' : ''
					}
			return True;
		
	def display_file(self, text, filename):
		"""
		Display text as plain text
		
		Filename is used to guess the `Content-Type` using :func:`mimetypes.guess_type`
		"""
		if filename.endswith('.svg'):
			self.response.headers['Content-Type'] = 'image/svg+xml'
		else:
			self.response.headers['Content-Type'] = mimetypes.guess_type(filename)[0] or 'text/plain'
		self.response.headers['Cache-Control'] = 'max-age=%s'%self.file_expires
		self.response.out.write(text)

	def display_article(self, name, extension=''):
		"""
		Display a requested article.
		
		if extension is specified, render as a file in requested format using :func:`display_file` :
			* xml: :func:`notewiki.model.Article.to_xml`
			* html: article.text with :func:`notewiki.ui.text_to_html`
			* md: title, tags, text in plain text using :func:`display_file`
			* other: article.text in Plain text using :func:`display_file`
		"""
		article = Article.get_by_key_name(name)
		if not article and (name == 'home'):
			article = Article(name='home', text='Article "home" not found !')
		if article:
			if extension:
				logging.debug("Display %s as %s"%(name, extension))
				if extension == 'xml':
					text = article.to_xml()
				elif extension == 'html':
					text = text_to_html(article.text, article, self.app.config.get('wiki_base_url'))
				elif extension == 'md':
					text = """Title: %s\nTags: %s\n%s"""%(article.title, tags_to_string(article.tags), article.text)
				else:
					text = article.text
				self.display_file(text, self.url)
			else:
				logging.info("Display Article %s"%name)
				self.context.update(article.to_context())
				if '_file_' in article.tags:
					self.context['presentation_type'] = 'plain'
					# use tags[1] to specify file language
					if len(article.tags) > 1:
						language =  article.tags[1]
					else:
						language = 'text'
					file_text = ("~~~~{.%s}\n"%language)+article.text+"\n~~~~\n"
					self.context['presentation'] = text_to_html(file_text, article, self.app.config.get('wiki_base_url'))
				else:
					self.context['presentation_type'] = 'html'
					self.context['presentation'] = text_to_html(article.text, article, self.app.config.get('wiki_base_url'))
				self.context['article_list'] = self.get_visible_article_list(article)
				parent = article.get_parent()
				if parent:
					self.context['parent_list'] = self.get_visible_article_list(parent)
				else:
					self.context['parent_list'] = []
				pages.ArticlePage(self.response, self.context)
		else:
			logging.debug("Article %s not found."%name)
			self.response.set_status(404)
			pages.ArticleNotFoundPage(self.response, {'name': name})
			
	def display_form(self, article, context={}):
		"""
		Use :class:`notewiki.ui.pages.ArticleFormPage`
		"""
		article_context = article.to_context()
		article_context['title'] = cgi.escape(article_context['title'], True) 
		article_context['text'] = cgi.escape(article_context['text'], True) 
		self.context.update(article_context)
		self.context.update(context)
		pages.ArticleFormPage(self.response, self.context)		

	def display_move_form(self, article, context={}):
		"""
		Use :class:`notewiki.ui.pages.ArticleMovePage`
		"""
		self.context.update(article.to_context())
		self.context.update(context)
		pages.ArticleMovePage(self.response, self.context)		
		
	def edit_article(self, name):
		"""
		Uses :func:`display_form` to display article in a form.
		"""
		article = Article.get_by_key_name(name)
		if article:
			logging.info("Edit Article %s"%name)
			self.context['command'] = 'edit'
			self.context['timestamp'] = time.time()
			self.display_form(article)
		else:
			logging.debug("Article %s not found."%name)
			self.response.set_status(404)
			pages.NotFoundPage(self.response,  {'url': name})
		
	def move_article(self, name):
		"""
		Display move form with :func:`display_move_form`
		"""
		article = Article.get_by_key_name(name)
		if article:
			logging.info("Move Article %s"%name)
			self.context['command'] = 'move'
			self.context['timestamp'] = time.time()
			self.display_move_form(article)
		else:
			logging.debug("Article %s not found."%name)
			self.response.set_status(404)
			pages.NotFoundPage(self.response,  {'url': name})

	def new_article(self):
		"""
		Use :func:`display_form`
		"""
		article = Article(key_name=self.name)
		logging.info("New Article %s"%self.name)
		self.context['command'] = 'new'
		self.display_form(article)

	def get_visible_article_list(self, article):
		"""
		Use :func:`notewiki.model.Article.list_all_visible` to return list.
		"""
		return Article.list_all_visible(self.user, article.name)	
		
	def list(self):
		"""
		Display list using :class:`notewiki.ui.pages.ArticleListPage`
		"""
		if not self.name:
			# a name is mandatory
			# article is used only as context for template
			self.name = ' '
		article = Article(key_name=self.name)
		visible_list = self.get_visible_article_list(article)
		self.context.update(article.to_context())
		self.context.update({
							'article_list': visible_list,
							})
		logging.info("Display list %s"%self.name)
		pages.ArticleListPage( self.response, self.context)

		
	def search(self):
		"""
		Use :func:`notewiki.model.Article.list_with_regex` to compose article list.
		Display list using :class:`notewiki.ui.pages.ArticleListPage`
		"""
		exp = self.request.params['exp']
		match_list = Article.list_with_regex(self.user, exp)
		# a name is mandatory
		# article is used only as context for template
		#article = Article(key_name=self.name)		
		#self.context.update(article.to_context())
		self.context.update({
							'title': 'search:%s'%exp,
							'article_list': match_list,
							})
		logging.info("Display match list for %s"%exp)
		pages.ArticleListPage( self.response, self.context)

	def delete_article(self):
		"""
		Use :func:`notewiki.model.Article.delete` to delete article.
		Redirect to folder.
		"""
		article = Article(key_name=self.name)
		if article:
			logging.info("Delete Article %s"%self.name)
			article.delete()
			if article.folder:
				self.redirect(self.app.config.get('wiki_base_url')+article.folder+'/')
			else:
				self.redirect(self.app.config.get('wiki_base_url'))
		else:
			logging.debug("Article %s not found."%self.name)
			self.response.set_status(404)
			pages.NotFoundPage(self.response, {'root_url': '', 'url': self.name})

	def article_from_form(self):
		"""
		Return an article composed with postdata:
		
			* name
			* title
			* text
			* tag
		"""
		params = self.request.params
		return Article(key_name=params['name'], name=params['name'], title=notewiki.ui.unescape(params['title']), text=notewiki.ui.unescape(params['text']), tag=params['tag'])
	
	def update_article(self):
		"""
		Use :func:`notewiki.model.Article.add_or_replace` to create or update article from :func:àrticle_from_form`
		
		Display updayed articmle using :func:`display_article`
		"""
		params = self.request.params
		key_name = None
		kwds = {}
		for (name, value) in params.items():
			if name == 'name':
				key_name = value
			kwds[name]=value
		form_article = self.article_from_form()
		article = Article.get_by_key_name(kwds['name'])
		if kwds.has_key('command') and (kwds['command'] == 'new'):
			article = Article.get_by_key_name(kwds['name'])
			if article:
				msg = "You try to create an existing article: %s"%kwds['name']
				logging.debug(msg)
				self.display_form(form_article, {'command' : kwds['command'], 'msg': msg})
				return
		if not re.match(r'^([a-z0-9\-_][a-z0-9\-_/]*)?[a-z0-9\-_]$', kwds['name']):
			msg = "Name is invalid. Name can contain only 'a-z0-9-_/' and no '/' in first and last position."
			logging.debug(msg)
			self.display_form(form_article, {'command' : kwds['command'], 'msg': msg})
			return
		if not re.match(r'^(( *[a-z0-9\-_]+ *, *)* *([a-z0-9\-_]+) *)?$', kwds['tag']):
			msg = "Tags are invalids. <br>A tag can contain only 'a-z0-9_'. Tags must be separated by \",\"."
			logging.debug(msg)
			self.display_form(form_article, {'command' : kwds['command'], 'msg': msg})
			return
		if article and kwds.has_key('timestamp') and (article.modified > datetime.datetime.fromtimestamp(float(kwds['timestamp']))):
			msg = "Article stored is newer than form's one. You should review and merge your change with stored version"
			logging.debug(msg)
			self.display_form(form_article, {'command' : kwds['command'], 'msg': msg})
			return
		logging.info("create/update article %s"%self.name)
		article = Article.add_or_replace(**kwds)
		if self.name == kwds['name']:
			self.display_article(self.name, self.name_extension)
		else:
			self.redirect(self.app.config.get('wiki_base_url')+kwds['name'])

	def rename_article(self):
		"""
		Create new article from `name` postdata with `new_name` postdata.
		Delete article with `name`.
		Display new article.
		"""
		params = self.request.params
		old_name = params['name']
		new_name = params['new_name']
		article = Article.get_by_key_name(old_name)
		if not re.match(r'^([a-z0-9\-_][a-z0-9\-_/]*)?[a-z0-9\-_]$', new_name):
			msg = "New Name is invalid. Name can contain only 'a-z0-9-_/' and no '/' in first and last position."
			logging.debug(msg)
			self.display_move_form(article, {'new_name' : new_name, 'msg': msg})
			return
		new_article = Article.get_by_key_name(new_name)
		if new_article:
			msg = "You try to rename to an existing article: %s"%new_name
			logging.debug(msg)
			self.display_move_form(article, {'new_name' : new_name, 'msg': msg})
			return
		logging.info("rename article %s"%self.name)
		new_article = article.rename(new_name)
		self.redirect(self.app.config.get('wiki_base_url')+new_name)
		
	def get(self, url):
		"""
		Handle get article or folder.
		
		Handle:
			`GET /{article_name}[.{extension}][?command]`
		
		If no article_name, take the first article with tag home if no article, article_name=home.

		Issue controls and dispatch to dedicated functions.

		with command : 
			* [display]: :func:`display_article`
			* edit: :func:`edit_article`
			* new: :func:`new_article`
			* delete: :func:`delete_article`
			* move: :func:`move_article`
			* list: :func:`list`
				
		"""
		if self.check_access(url, write_access = False):
			if self.command == 'list':
				self.list()
			elif not self.name:	
				article_list = Article.list_with_tag('home')
				if len(article_list) > 0:
					self.name = article_list[0].name
				else:
					self.name = 'home'
			if (self.command == '') or (self.command == 'display'):
				self.display_article(self.name, self.name_extension)
			elif self.command == 'edit':
				if self.user_role < Role.contributor:
					self.response.set_status(401)
					pages.ForbiddenPage(self.response, {'url': self.name})
					return	
				self.edit_article(self.name)			
			elif self.command == 'new':
				if self.user_role < Role.contributor:
					self.response.set_status(401)
					pages.ForbiddenPage(self.response, {'url': self.name})
					return
				self.new_article()	
			elif self.command == 'delete':
				if self.user_role < Role.contributor:
					self.response.set_status(401)
					pages.ForbiddenPage(self.response, {'url': self.name})
					return
				self.delete_article()				
			elif self.command == 'move':
				if self.user_role < Role.contributor:
					self.response.set_status(401)
					pages.ForbiddenPage(self.response, {'url': self.name})
					return	
				self.move_article(self.name)			

			
	def post(self, url):
		"""
		Handle post article.
		
		Handle:
			`POST /{article_name}`
		
		For now, article_name is not used.
		
		postdata should contains:
			* command : 
				* search: :func:`search`
				* move: :func:`rename_article`
				* (update or create): :func:`update_article`
			
		Issue controls and dispatch to dedicated functions.
		"""
		if self.check_access(url, write_access = True):
			if self.command == 'search':
				self.search()
			elif self.command == 'move':
				rename_user_role = user_role(self.request.params['new_name'])
				if rename_user_role > Role.reader: 
					self.rename_article()
				else:
					self.response.set_status(401)
					pages.ForbiddenPage(self.response, {'url': self.request.params['new_name']})
			else:
				self.update_article()

	
class ProfileHandler(webapp2.RequestHandler):
	"""
	Class to manage Profile. 
	
	Work in progress...
	
	It's to manage User profile in an notewiki.model.Profile entity.
	"""
	
	def init_profile(self, name):
		self.user = users.get_current_user()
		self.profile = Profile.get_for_name(name) or Profile(name=render_user(self.user), guser=self.user)
		self.command = self.request.query_string or 'display'
			
	def render_profilePage(self, msg = ''):
		if self.command == 'display':
			read_only = True
		else:
			read_only = False
		self.context = {
				'app_name': self.app.config.get('wiki_app_name'),
				'title': 'profile',
				'path': self.request.path_info,
				'command': self.command,
				'user': self.app.config.get('format_user_fn')(self.request.url, self.app.config.get('wiki_base_url')),
				'username': self.profile.real_name or render_user(self.user),
				'profile': self.profile,
				'read_only': read_only,
				'msg' : msg
					}
		pages.ProfilePage(self.response, self.context)		
		

	def get(self, group, name):
		"Display profile"
		self.init_profile(name)
		self.render_profilePage()

	def post(self, group, name):
		"Update profile"
		self.init_profile(name)
		# controls ???
		self.profile.real_name = self.request.params['real_name']
		self.profile.name = self.request.params['name']
		self.profile.bio = self.request.params['bio']
		self.profile.title = self.request.params['title']
		self.profile.web = self.request.params['web']
		self.profile.photo = self.request.params['photo']
		if self.request.params.has_key('tos'):
			self.profile.tos = True
		self.profile.put()
		self.render_profilePage("Profile saved")

			
class TagHandler(ControledRequestHandler):	
	"""
	Handler for tags
	"""
	
	def get(self, tag, extension):
		"""
		List all visible article with tag.
		
		If extension is `.xml` use :func:`notewiki.model.Article.list_to_xml` to list article as plain text.
		
		Else uses :func:`notewiki.ui.pages.ArticleListPage` to render list.
		"""
		if self.check_access(self.request.path_info):
			self.tag = tag
			article_list = Article.list_with_tag(self.tag)
			visible_list = []
			for a in article_list:
				role = user_role(a.name)
				if role >= Role.reader:
					visible_list.append(a)
			if extension == '.xml' :
				xml = Article.list_to_xml(visible_list)
				self.response.headers['Content-Type'] = 'text/xml'
				self.response.out.write(xml)
				logging.info("Display list for tag %s as 'txt/xml'"%self.tag)
			else:
				self.context = {
					'app_name':  self.app.config.get('wiki_app_name'),
					'path': self.request.path_info,
					'command': 'tag',
					'user': self.app.config.get('format_user_fn')(self.request.url, self.app.config.get('wiki_base_url')),
					'name': 'tags/'+self.tag,
					'title': 'tags/'+self.tag,
					'msg' : ''
							}
				self.context.update({
									'folders': None,
									'article_list': visible_list,
									})
				logging.info("Display list for tag %s"%self.tag)
				pages.ArticleListPage( self.response, self.context)

class TagListHandler(ControledRequestHandler):	
	"""
	Handler for tag list
	"""
	
	def get(self):
		"""
		Uses :func:`notewiki.model.Article.get_all_tags` to get a tag list.
		Renders tag list using :class:`notewiki.ui.pages.TagListPage`
		"""
		if self.check_access(self.request.path_info):
			tags = Article.get_all_tags(self.user)
			# sorted by tag in alphabetic order
			#sorted_tags = sorted(tags.items(), key=itemgetter(0), reverse=False)
			# sorted by use
			sorted_tags = sorted(tags.items(), key=itemgetter(1), reverse=True)
			self.context = {
				'app_name':  self.app.config.get('wiki_app_name'), 
				'user': self.app.config.get('format_user_fn')(self.request.url, self.app.config.get('wiki_base_url')),
				'username': render_user(self.user),
				'title': 'tags',
				'tags': sorted_tags,
							}
			logging.info("Display tag list")
			pages.TagListPage( self.response, self.context)


class UserHandler(webapp2.RequestHandler):
	def get(self, group, name):
		"Display profile"
		self.profile = Profile.get_for_name(name)
		if self.profile:
			self.context = {
					'app_name': self.app.config.get('wiki_app_name'),
					'path': self.request.path_info,
					'command': 'display',
					'user': self.app.config.get('format_user_fn')(self.request.url, self.app.config.get('wiki_base_url')),
					'username': self.profile.name,
					'profile': self.profile,
					'html_bio': text_to_html(self.profile.bio, None, self.app.config.get('wiki_base_url')),
					'email': self.profile.guser.email(),
					'read_only': True,
					'msg' : ''
						}
			pages.UserPage(self.response, self.context)
		else:
			self.context = {
					'app_name': self.app.config.get('wiki_app_name'),
					'path': self.request.path_info,
					'msg' : 'No profile for name %s'%name,
						}
			
			pages.NotFoundPage(self.response, self.context)		


class XmlExport(ControledRequestHandler):
	"""
	Handler for exporting Notewiki articles as an XML document.
	If tag is specified uses :func:`notewiki.model.Article.list_with_tag` to get article list.

	Idea from http://stackoverflow.com/questions/426820
	"""
	def get(self):
		"""
		Returns an XML document with articles in XML Format.
		
		if parameter tag is specified, articles with tag are exported. If tag parameter is not 
		specified all articles are exported.
		
		Args:
			tag(String): optional, tag to select articles
			
		Returns:
			XML document
		"""
		if self.check_admin_access(self.request.path_info):
			if self.request.params.has_key('tag'): 
				xml = Article.list_to_xml(Article.list_with_tag(self.request.params['tag']))
			else:
				xml = Article.all_to_xml()
			self.response.headers['Content-Type']='text/xml; charset=utf-8'
			self.response.headers['Cache-Control'] = 'no-cache'
			self.response.out.write(xml)
			logging.info("XMLExport - file exported")

class XmlImport(ControledRequestHandler):
	"""
	Handler for importing article in XML document 
	Idea from http://stackoverflow.com/questions/426820
	"""
	def get(self):
		"""
		Uses :class:`notewiki.ui.pages.ImportPage` to display a form to select a file.
		"""
		if self.check_admin_access(self.request.path_info):		
			pages.ImportPage(self.response, {'app_name':  self.app.config.get('wiki_app_name'), 'title': 'import', 'path': self.request.path_info})
		
	def post(self):
		"""
		Receive a file as postdata. Import (create or update) all articles parse using :func:`notewiki.model.Article.set_from_xml` 
		"""
		if self.check_admin_access(self.request.path_info):		
			xml = self.request.get('file')
			Article.set_from_xml(xml)
			msg = "file imported successfully"
			logging.warn("XMLImport - file imported successfully")
			pages.ImportPage(self.response, {'app_name':  self.app.config.get('wiki_app_name'), 'title': 'import', 'path': self.request.path_info, 'msg': msg})
		

def notewikiapp(debug, config):
	"""
	Notewiki App router.
	
	Associates handlers to url.
	
	Returns a :class:`webapp2.WSGIApplication`
	"""
	config['format_user_fn'] = notewiki.ui.format_user
	return webapp2.WSGIApplication([
								('/favicon\.ico', StaticPageHandler),
								('((/css)|(/images)|(/js))(.*)', StaticPageHandler),
								('/tags/*', TagListHandler),
								('/tags/(.*?)(\..*)*', TagHandler),
								# for now profile and users are work in progress... not activated
								# ('/profile(/(.*))?', ProfileHandler),
								# ('/users(/(.*))?', UserHandler),
								('/_admin/export', XmlExport),
								('/_admin/import', XmlImport),
								('/(.*)', ArticleHandler),
								('.*', StaticPageHandler)],
                             debug=debug,
                             config=config)
